package bma.common.langutil.ai.objectpool;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import bma.common.langutil.ai.common.AIEvent;
import bma.common.langutil.ai.common.AIFunction;
import bma.common.langutil.ai.stack.AIStack;
import bma.common.langutil.ai.stack.AIStackStep;
import bma.common.langutil.concurrent.ProcessTimerTask;
import bma.common.langutil.concurrent.ProcessTimerTaskAbstract;
import bma.common.langutil.concurrent.TimerManager;
import bma.common.langutil.core.DateTimeUtil;

public class GenericAIKeyedObjectPool<KTYPE, VTYPE> implements
		AIKeyedObjectPool<KTYPE, VTYPE> {

	final org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(GenericAIKeyedObjectPool.class);

	private int maxActive = 0;
	private long waitTimeout = 5 * 1000;
	private int maxIdle = 0;
	private int minIdle = 0;
	private long idleMaxTime = 0;
	private long evictIdleTime = 5 * 1000;
	private boolean testOnBorrow = true;
	private boolean testOnReturn = true;
	private boolean testWhileIdle = true;
	private boolean removeEmptyPool = true;
	private boolean lifo = true;
	private int initPoolSize = 16;

	private AIKeyedPoolableObjectFactory<KTYPE, VTYPE> factory;
	private TimerManager timer;

	// runtime
	private boolean close;

	static class POOLOBJ<POTYPE> {
		long idleTime;
		POTYPE value;
	}

	static class POOL<PTYPE> {
		int active;
		LinkedList<POOLOBJ<PTYPE>> idleList = new LinkedList<POOLOBJ<PTYPE>>();
		boolean timer;
		AIEvent<PTYPE> waiting;
	}

	private ConcurrentHashMap<KTYPE, POOL<VTYPE>> poolMap;

	public GenericAIKeyedObjectPool() {
		super();
	}

	public long getWaitTimeout() {
		return waitTimeout;
	}

	public void setWaitTimeout(long waitTimeout) {
		this.waitTimeout = waitTimeout;
	}

	public boolean isTestOnReturn() {
		return testOnReturn;
	}

	public void setTestOnReturn(boolean testOnReturn) {
		this.testOnReturn = testOnReturn;
	}

	public boolean isLifo() {
		return lifo;
	}

	public void setLifo(boolean lifo) {
		this.lifo = lifo;
	}

	public boolean isRemoveEmptyPool() {
		return removeEmptyPool;
	}

	public void setRemoveEmptyPool(boolean removeEmptyPool) {
		this.removeEmptyPool = removeEmptyPool;
	}

	public int getInitPoolSize() {
		return initPoolSize;
	}

	public void setInitPoolSize(int initPoolSize) {
		this.initPoolSize = initPoolSize;
	}

	public AIKeyedPoolableObjectFactory<KTYPE, VTYPE> getFactory() {
		return factory;
	}

	public void setFactory(AIKeyedPoolableObjectFactory<KTYPE, VTYPE> factory) {
		this.factory = factory;
	}

	public int getMaxActive() {
		return maxActive;
	}

	public void setMaxActive(int maxActive) {
		this.maxActive = maxActive;
	}

	public int getMaxIdle() {
		return maxIdle;
	}

	public void setMaxIdle(int maxIdle) {
		this.maxIdle = maxIdle;
	}

	public int getMinIdle() {
		return minIdle;
	}

	public void setMinIdle(int minIdle) {
		this.minIdle = minIdle;
	}

	public long getIdleMaxTime() {
		return idleMaxTime;
	}

	public void setIdleMaxTime(long idleTime) {
		this.idleMaxTime = idleTime;
	}

	public boolean isTestOnBorrow() {
		return testOnBorrow;
	}

	public void setTestOnBorrow(boolean testOnBorrow) {
		this.testOnBorrow = testOnBorrow;
	}

	public boolean isTestWhileIdle() {
		return testWhileIdle;
	}

	public void setTestWhileIdle(boolean testWhileIdle) {
		this.testWhileIdle = testWhileIdle;
	}

	public long getEvictIdleTime() {
		return evictIdleTime;
	}

	public void setEvictIdleTime(long testIdleTime) {
		this.evictIdleTime = testIdleTime;
	}

	public TimerManager getTimer() {
		return timer;
	}

	public void setTimer(TimerManager timer) {
		this.timer = timer;
	}

	public boolean isClose() {
		return close;
	}

	public void setClose(boolean close) {
		this.close = close;
	}

	public ConcurrentHashMap<KTYPE, POOL<VTYPE>> getPoolMap() {
		if (this.poolMap == null) {
			synchronized (this) {
				if (this.poolMap == null) {
					this.poolMap = new ConcurrentHashMap<KTYPE, POOL<VTYPE>>(
							initPoolSize);
				}
			}
		}
		return this.poolMap;
	}

	public POOL<VTYPE> getPool(KTYPE key, boolean sure) {
		POOL<VTYPE> pool = getPoolMap().get(key);
		if (pool == null && sure) {
			ConcurrentHashMap<KTYPE, POOL<VTYPE>> pm = getPoolMap();
			pm.putIfAbsent(key, new POOL<VTYPE>());
			pool = pm.get(key);
		}
		return pool;
	}

	@Override
	public boolean borrowObject(AIStack<VTYPE> stack, final KTYPE key) {
		while (true) {
			if (isClose()) {
				return stack.failure(new IllegalStateException("pool closed"));
			}
			POOL<VTYPE> pool = getPool(key, true);
			POOLOBJ<VTYPE> obj = null;
			synchronized (pool) {
				if (!pool.idleList.isEmpty()) {

					if (lifo) {
						obj = pool.idleList.removeLast();
					} else {
						obj = pool.idleList.removeFirst();
					}

				}
			}
			if (obj != null) {
				if (activeObject(key, obj.value)) {
					synchronized (pool) {
						pool.active++;
					}
					if (log.isDebugEnabled()) {
						log.debug("borrow idle [{},{}]", key, obj.value);
					}
					return stack.success(obj.value);
				}
			}
			// TODO: maxActive
			if (maxActive > 0) {
				synchronized (pool) {
					if (pool.active >= maxActive) {
						if (pool.waiting == null) {
							pool.waiting = new AIEvent<VTYPE>(false);
						}
						if (log.isDebugEnabled()) {
							log.debug("maxActive reach [{},{}] - wait", key,
									pool.active);
						}
						pool.waiting.waitEvent(new AIFunction<VTYPE>() {
							@Override
							public boolean apply(AIStack<VTYPE> stack) {
								return borrowObject(stack, key);
							}
						}, stack, waitTimeout);
					}
				}
			}
			AIStack<VTYPE> st = new AIStackStep<VTYPE, VTYPE>(stack) {
				protected boolean next(VTYPE result) {
					POOL<VTYPE> pool = getPool(key, true);
					if (activeObject(key, result)) {
						synchronized (pool) {
							pool.active++;
						}
						if (log.isDebugEnabled()) {
							log.debug("create new [{},{}]", key, result);
						}
						return successForward(result);
					}
					return borrowObject(delegate(), key);
				};
			};
			return factory.makeObject(st, key);
		}
	}

	private boolean activeObject(KTYPE key, VTYPE obj) {
		// activate & validate the object
		try {
			factory.activateObject(key, obj);
			if (testOnBorrow && !factory.validateObject(key, obj)) {
				if (log.isDebugEnabled()) {
					log.debug("validate object failed");
				}
				return false;
			}
			return true;
		} catch (Throwable e) {
			destroyObject(key, obj);
			return false;
		}
	}

	@Override
	public void returnObject(KTYPE key, VTYPE obj) {
		try {
			addObjectToPool(key, obj, true);
		} catch (Exception e) {
			if (factory != null) {
				destroyObject(key, obj);
				POOL<VTYPE> pool = getPool(key, false);
				if (pool != null) {
					synchronized (pool) {
						pool.active--;
						_checkPool(key, pool);
					}
				}
			}
		}
	}

	@Override
	public void dropObject(KTYPE key, VTYPE obj) {
		if (factory != null) {
			destroyObject(key, obj);
		}
		POOL<VTYPE> pool = getPool(key, false);
		if (pool != null) {
			synchronized (pool) {
				pool.active--;
				_checkPool(key, pool);
			}
		}
	}

	private void addObjectToPool(KTYPE key, VTYPE obj,
			boolean decrementNumActive) throws Exception {

		boolean shouldDestroy = isClose();
		factory.passivateObject(key, obj);
		if (testOnReturn && !factory.validateObject(key, obj)) {
			shouldDestroy = true;
		}
		if (shouldDestroy && !decrementNumActive) {
			destroyObject(key, obj);
			return;
		}

		POOL<VTYPE> pool = getPool(key, true);
		AIEvent<VTYPE> event = null;
		synchronized (pool) {
			if (decrementNumActive) {
				pool.active--;
			}
			if (!shouldDestroy) {
				// if there's no space in the pool, flag the object for
				// destruction
				// else if we passivated successfully, return it to the pool
				if (maxIdle > 0 && (pool.idleList.size() >= maxIdle)) {
					if (log.isDebugEnabled()) {
						log.debug("pool idle full [{},{}]", key,
								pool.idleList.size());
					}
					shouldDestroy = true;
				} else {
					if (log.isDebugEnabled()) {
						log.debug("add idle object [{},{}]", key, obj);
					}
					// borrowObject always takes the first element from the
					// queue,
					// so for LIFO, push on top, FIFO add to end
					POOLOBJ<VTYPE> pobj = new POOLOBJ<VTYPE>();
					pobj.idleTime = System.currentTimeMillis();
					pobj.value = obj;
					pool.idleList.addLast(pobj);

					if (idleMaxTime > 0 && this.evictIdleTime > 0
							&& pool.idleList.size() > minIdle && !pool.timer) {
						final KTYPE checkKey = key;
						ProcessTimerTask task = new ProcessTimerTaskAbstract(
								System.currentTimeMillis() + evictIdleTime) {
							@Override
							public void run() {
								if (evict(checkKey)) {
									this.setTime(System.currentTimeMillis()
											+ evictIdleTime);
									timer.postTimerTask(this);
								}
							}
						};
						timer.postTimerTask(task);
						pool.timer = true;
					}

					event = pool.waiting;
				}
			}
			_checkPool(key, pool);
		}

		if (event != null) {
			try {
				event.setEvent();
			} catch (Exception e) {
			}
		}

		// Destroy the instance if necessary
		if (shouldDestroy) {
			destroyObject(key, obj);
		}
	}

	private void destroyObject(KTYPE key, VTYPE obj) {
		if (log.isDebugEnabled()) {
			log.debug("destroy object [{},{}]", key, obj);
		}
		try {
			factory.destroyObject(key, obj);
		} catch (Exception e) {
			// ignored?
		}
	}

	private void _checkPool(KTYPE key, POOL<VTYPE> pool) {
		if (removeEmptyPool && pool.active <= 0 && pool.idleList.isEmpty()) {
			getPoolMap().remove(key);
			if (log.isDebugEnabled()) {
				log.debug("remove empty pool [{}]", key);
			}
			if (pool.waiting != null) {
				pool.waiting
						.allFailure(new IllegalStateException("pool close"));
			}
		}
	}

	protected boolean evict(KTYPE key) {
		POOL<VTYPE> pool = getPool(key, false);
		if (pool == null)
			return false;
		boolean r = true;
		List<POOLOBJ<VTYPE>> tmp = new ArrayList<POOLOBJ<VTYPE>>();
		synchronized (pool) {
			long tm = System.currentTimeMillis() - this.idleMaxTime;
			ListIterator<POOLOBJ<VTYPE>> it = pool.idleList.listIterator();
			while (it.hasNext() && pool.idleList.size() > minIdle) {
				POOLOBJ<VTYPE> obj = it.next();
				if (obj.idleTime > tm)
					break;
				tmp.add(obj);
				it.remove();
			}

			if (pool.idleList.size() <= minIdle) {
				pool.timer = false;
				_checkPool(key, pool);
				r = false;
			}
		}
		if (!tmp.isEmpty()) {
			for (POOLOBJ<VTYPE> obj : tmp) {
				if (log.isDebugEnabled()) {
					log.debug(
							"evict idle obejct [{},{},{}]",
							new Object[] {
									key,
									DateTimeUtil.formatPeriod(System
											.currentTimeMillis() - obj.idleTime),
									obj });
				}
				destroyObject(key, obj.value);
			}
			tmp.clear();
		}
		return r;
	}

	public void close() {
		close = true;
		if (this.poolMap != null) {
			Set<KTYPE> keys = this.poolMap.keySet();
			for (KTYPE key : keys) {
				POOL<VTYPE> pool = this.poolMap.remove(key);
				List<POOLOBJ<VTYPE>> tmp;
				synchronized (pool) {
					tmp = new ArrayList<POOLOBJ<VTYPE>>(pool.idleList);
					pool.idleList.clear();
				}
				for (POOLOBJ<VTYPE> obj : tmp) {
					destroyObject(key, obj.value);
				}
				AIEvent<VTYPE> event;
				synchronized (pool) {
					event = pool.waiting;
				}
				if (event != null) {
					event.allFailure(new IllegalStateException("pool close"));
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("AIPool[");
		if (this.poolMap != null) {
			for (Map.Entry<KTYPE, POOL<VTYPE>> e : this.poolMap.entrySet()) {
				sb.append(e.getKey()).append("<");
				POOL<VTYPE> pool = e.getValue();
				synchronized (pool) {
					sb.append("A:").append(pool.active).append(";I:")
							.append(pool.idleList.size());
				}
				sb.append(">");
			}
		}
		sb.append("]");
		return sb.toString();
	}

}
